#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import logging
import os
import sys
import yaml
import traceback

from oriolus.gaffer.gaffer.catalogue import Catalogue
from oriolus.gaffer.gaffer.selectstrategy import LatestStrategy
from oriolus.gaffer.cache import Cache
from oriolus.docker.docker import DockerError, DockerFactory, DockerRemoveError
from oriolus.pith.packets.configreader import ConfigReader
from oriolus.pith.packets.metareader import FileMetaReader
from oriolus.pith.packets.packet import FilePacket, FullName, Status, FullNameParseError
from oriolus.trotter.trotter import TrotterFactory

class GafferError(Exception):
    def __init__(self, message):
        Exception.__init__(self, message)
        
class GafferFactoryError(Exception):
    def __init__(self, message):
        Exception.__init__(self, message)

def __logerror__(logger, message):
    if logger.isEnabledFor(logging.DEBUG):
        logger.exception(message)
    else:
        logger.error(message)
        
class Gaffer:
    def __init__(self, cache, docker, trotter, catalogue, select_strategy):
        self.__cache = cache
        self.__docker = docker
        self.__trotter = trotter
        self.__catalogue = catalogue
        self.__select_strategy = select_strategy
        self.__logger = logging.getLogger("Gaffer")

    def update_list(self):
        self.__trotter.update()

    def install(self, packet_names, virtual_docker = None, base_path = None):
        self.__logger.info("Packet names were passed to install: %s" % str(packet_names))
        fitted_full_names = self.__catalogue.identify_packets(packet_names)
        selected_full_names = self.__select_strategy.select_packets(fitted_full_names)
        self.__logger.info("Packet names will be installed: %s" % map(str, selected_full_names))

        if virtual_docker is None:
            virtual_docker = DockerFactory().get_virtual_docker(self.__docker)

        full_names_to_replace = self.__find_packets_to_replace(selected_full_names, self.__docker)

        try:
#            self.__logger.info("Try to remove full_names_to_replace: %s" % map(str,full_names_to_replace))
            ordered_replaces = self.__remove_packets(full_names_to_replace, virtual_docker)
#            self.__logger.info("getted ordered_replaces: %s" % map(str,ordered_replaces))
            self.__install_packets(selected_full_names, virtual_docker, base_path)
        except DockerError, error:
            message = "Installation of the packets %s can not be done! %s" % (map(str, selected_full_names), error)
            __logerror__(self.__logger, message) 
            raise GafferError(message)
        else:
            try:
#                self.__logger.info("Remove ordered_replaces: %s" % map(str,ordered_replaces))
                self.__remove_packets(ordered_replaces, self.__docker)
#                self.__logger.info("Install selected_full_names: %s" % map(str, selected_full_names))
                self.__install_packets(selected_full_names, self.__docker, base_path)
            except DockerError, error:
                message = "An error has been occurred during installation of the packets %s! " \
                                  "Error description: %s" % (map(str, selected_full_names), error)
                __logerror__(self.__logger, message) 
                raise GafferError(message)

    def remove(self, packet_names):
        self.__logger.info("Packet names were passed to remove: %s" % str(packet_names))
        full_names_to_remove = map(FullName.parse, packet_names)

        virtual_docker = DockerFactory().get_virtual_docker(self.__docker)

        try:
#            self.__logger.info("try remove packets from virtual docker")
            ordered_full_names_to_remove = self.__remove_packets(full_names_to_remove, virtual_docker)
        except DockerError, error:
            message = "Remove of the packets [%s] can not be done! %s" % (", ".join(packet_names), error)
            __logerror__(self.__logger, message) 
            raise GafferError(message)
        else:
            try:
#                self.__logger.info("remove packets from docker")
                self.__remove_packets(ordered_full_names_to_remove, self.__docker)
            except DockerError, error:
                message = "An error has been occurred during remove of the packets [%s]! " \
                                  "Error description: %s" % (", ".join(packet_names), error)
                __logerror__(self.__logger, message) 
                raise GafferError(message)

    def upgrade(self, upgrade_list = [], base_path = None):
        installed_packets = self.__docker.search(get_all = True)
        installed_packets_full_names = [packet.name for packet in installed_packets]
        listed_packets = self.__trotter.get_packet_list()
        if not upgrade_list in [None, []]:
            listed_packets = self.__get_packets_for_upgrade(listed_packets, upgrade_list)
            
        self.__logger.debug("Installed packets before upgrade: %s" % str(installed_packets_full_names))
        self.__logger.debug("Available packets before upgrade: %s" % str(listed_packets))
                       
        full_names = map(FullName.parse, installed_packets_full_names + listed_packets)        
        selected_full_names = self.__select_strategy.select_packets(full_names) 

        full_names_to_remove = []
        full_names_to_install = []

        for selected_full_name in selected_full_names:
            name = selected_full_name.name
            installed = [FullName.parse(full_name) for full_name in installed_packets_full_names if FullName.parse(full_name).name == name]
            if len(installed) == 0:
                continue 

            if str(selected_full_name) in listed_packets:
                if selected_full_name in installed:      
                    to_remove = [full_name for full_name in installed if full_name != selected_full_name]
                    full_names_to_remove.extend(to_remove)
                else:
                    full_names_to_remove.extend(installed)
                    full_names_to_install.append(selected_full_name)
            else:
                self.__logger.warning("Packet '%s' is installed but omitted on servers" % str(selected_full_name))

        virtual_docker = DockerFactory().get_virtual_docker(self.__docker)

        full_names_to_replace = self.__find_packets_to_replace(full_names_to_install, self.__docker)
        for full_name in full_names_to_replace:
            if not full_name in full_names_to_remove:
                full_names_to_remove.append(full_name)

        self.__logger.info("Packets will be installed: %s" % map(str, full_names_to_install))
        self.__logger.info("Packets will be removed: %s" % map(str, full_names_to_remove))
        try:
#            self.__logger.info("try remove packets from virtual docker")
            self.__install_packets(full_names_to_install, virtual_docker, base_path)
            ordered_removes = self.__remove_packets(full_names_to_remove, virtual_docker)
        except DockerError, error:
            message = "Installation of the packets %s can not be done! %s" % (map(str, full_names_to_install), error)
            __logerror__(self.__logger, message)
            raise GafferError(message)
        else:
            try:
#                self.__logger.info("remove packets from docker")
                self.__install_packets(full_names_to_install, self.__docker, base_path)
                self.__remove_packets(ordered_removes, self.__docker)
            except DockerError, error:
                message = "An error has been occurred during installation of the packets %s! " \
                                  "Error description: %s" % (map(str, full_names_to_install), error)
                __logerror__(self.__logger, message)
                raise GafferError(message)
            
    def install_configuration(self, configuration_packets, base_path = None):
        installed_packets = self.__docker.search(status = Status.INSTALLED)
        installed_packet_names = [packet.name for packet in installed_packets]
        for_remove = list(set(installed_packet_names) - set(configuration_packets))
        for_install = list(set(configuration_packets) - set(installed_packet_names))
        
        self.remove(for_remove)
        self.install(for_install, base_path = base_path)

    def __get_packets_for_upgrade(self, listed_packets, upgrade_list):
        result = []
        
        for packet_name in upgrade_list:
            try:
                packet_full_name = FullName.parse(packet_name)
            except FullNameParseError:
                packet_full_name = FullName(packet_name, "")
                
            listed = [FullName.parse(full_name) for full_name in listed_packets if FullName.parse(full_name).name == packet_full_name.name]
            if packet_full_name.version != "" and listed != []:
                if packet_full_name in listed:
                    listed = [packet_full_name]

            result.extend(listed)
        
        return result

    def __find_packets_to_replace(self, packet_full_names, docker):
        replaces_full_names = []
        for full_name_object in packet_full_names:
            self.__find_replaces(docker, full_name_object, replaces_full_names)

        return replaces_full_names

    def __find_replaces(self, docker, full_name_object, replaces_full_names):
        if not self.__is_packet_installed(docker, str(full_name_object)):
            path = self.__locate_package(full_name_object)                
            self.__find_installed_replaces(docker, path, replaces_full_names)

    def __find_installed_replaces(self, docker, path, replaces_full_names):
        packet = FilePacket(path)
        meta = packet.meta

        for replace in meta.replaces:
            name = replace["Name"]
            all_packets = docker.search(name = name, status = Status.INSTALLED)
            packet_full_names_in_replaces = self.__get_versions_in_replaces(meta, all_packets)

            for full_name in packet_full_names_in_replaces:
                if not full_name in replaces_full_names:
                    replaces_full_names.append(full_name)

        for dependency in meta.depends:
            packet_fullname = self.__select_dependent_packet_full_name(dependency, meta)
            self.__find_replaces(docker, packet_fullname, replaces_full_names)

    def __get_versions_in_replaces(self, meta, packets):
        return [FullName(packet.meta.name, packet.meta.version) for packet in packets if meta.is_in_replaces(packet.meta.name, packet.meta.version)]

    def __select_dependent_packet_full_name(self, dependency, parent_meta):
        name = dependency["Name"]
        all_versions = self.__trotter.get_versions(name)
        dependent_packet_full_names = self.__get_dependent_versions(parent_meta, name, all_versions)
        packets_to_install = self.__select_strategy.select_packets(dependent_packet_full_names)

        return packets_to_install[0]

    def __remove_packets(self, packet_full_names, docker):
        successful_remove_sequence = []
#        self.__logger.info("packet_full_names %s" % map(str, packet_full_names))
        first_removal_list = self.__get_packets_list_for_remove(packet_full_names, docker)
#        self.__logger.info("first_removal_list %s" % map(str, first_removal_list))
        second_removal_list = []

        while first_removal_list: 
            for packet_full_name in first_removal_list:
                try:
                    docker.remove(str(packet_full_name))
                    if packet_full_name in second_removal_list:
                        second_removal_list.remove(packet_full_name)
                    if not packet_full_name in successful_remove_sequence:
                        successful_remove_sequence.append(packet_full_name)
                except DockerRemoveError, ex:
                    for dependent_packet in ex.dependent_packet_full_names:
                        if not dependent_packet in first_removal_list:
                            raise
                        if not dependent_packet in second_removal_list:
                            second_removal_list.append(dependent_packet)
                        if not packet_full_name in second_removal_list:
                            second_removal_list.append(packet_full_name)

            first_removal_list = second_removal_list
            second_removal_list = []

        return successful_remove_sequence

    def __get_packets_list_for_remove(self, packet_full_names, docker):
        result = []
        for packet_name in packet_full_names:
#            self.__logger.info("add deps for packet %s" % packet_name)
            result = self.__add_packet_with_deps(result, packet_name, docker, packet_full_names)
#            self.__logger.info("result of adding deps %s" % map(str, result))

        return self.__reduce_packet_list(result)

    def __reduce_packet_list(self, packet_list):
        result = []
        
        if len(packet_list) > 0:
            for packet in packet_list:
                if not packet in result:
                    result.append(packet)
                    
        return result

    def __add_packet_with_deps(self, result_list, packet_name, docker, packets_to_remove, status = Status.ALL):
        result = result_list
        
        #self.__logger.info("add depends for packet_name: %s, type: %s" % (packet_name, type(packet_name)))
        found_packets = docker.search(name = packet_name.name)
        packets = filter(lambda x: x.meta.version.startswith(packet_name.version), found_packets) 
        #self.__logger.info("find packets: %s" % (map(str, packets)))
        if len(packets) > 1:
            raise DockerRemoveError("Packet %s can not be removed. Reason: There are more then one packet witn this full name in repository" % packet_name, [])
              
        all_packets = docker.search(get_all = True)

        packet_names_to_remove = [str(x) for x in packets_to_remove]
        result_packet_names = [str(x) for x in result_list]   
              
        for packet in packets:
            if not packet.name in packet_names_to_remove:
                for packet_from_repo in all_packets:
                    if packet_from_repo.name in packet_names_to_remove or packet_from_repo.name in result_packet_names:
                        continue
                    
                    for dependency in packet_from_repo.meta.depends:
                        dep_name = dependency["Name"]
                        packet_name = packet.meta.name
                        dep_version = dependency["Version"]
                        packet_version = packet.meta.version
                        if dep_name == packet_name and packet_version.startswith(dep_version):
                            return result 
#            self.__logger.info("find packet: %s, packet status: %s, status for search: %s" % (packet.name, packet.status, status))
            if (status == Status.ALL and packet.status in [Status.INSTALLED, Status.INSTALLED_AS_DEPENDENCY, Status.BAD, Status.NEW]) or (status != Status.ALL and packet.status == status):
                result.append(FullName(packet.meta.name, packet.meta.version))
                depends = packet.meta.depends
                #self.__logger.info("find depends: %s, for packet: %s" % (depends, packet.name))
                for dependency in depends:
                    packet_name = FullName(dependency["Name"], dependency["Version"])
                    #self.__logger.info("find packet: %s" % packet_name)
                    result = self.__add_packet_with_deps(result, packet_name, docker, packets_to_remove, status = Status.INSTALLED_AS_DEPENDENCY)
            
        return result
    
    def __install_packets(self, packet_full_names, docker, base_path):
        for packet_full_name in packet_full_names:
            self.__install(packet_full_name, docker, Status.INSTALLING, base_path)

    def __install(self, full_name_object, docker, install_status, base_path):
#        self.__logger.info("Check for installed packet: %s, with status: %s" % (str(full_name_object), install_status))
        if not self.__is_packet_installed(docker, str(full_name_object), Status.status_map[install_status]):
            path = self.__locate_package(full_name_object)
            self.__install_uninstalled_dependencies(docker, path, base_path)
#            self.__logger.info("call install. packet_path %s, with status %s" % (path, install_status))
            docker.install(packet_path = path, base_path = base_path, status = install_status)

    def __is_packet_installed(self, docker, packet_fullname, install_status = Status.INSTALLED):
        installed_packets = docker.search(full_name = packet_fullname)
#        self.__logger.info("Number of find packets: %s" % len(installed_packets))
        if installed_packets in [None, []]:
            return False
        
        find_packet = installed_packets[0]
        if find_packet == None:
            return False
        
#        self.__logger.info("Find packet: %s, status: %s, install_status: %s" % (find_packet.name, find_packet.status, install_status))
        if find_packet.status == install_status:
            return True
        
        if find_packet.status == Status.INSTALLED and install_status == Status.INSTALLED_AS_DEPENDENCY:
            return True
        
        return False

    def __locate_package(self, full_name_object):
        abs_path = ""
        full_name = str(full_name_object)
        if self.__cache.is_stored(full_name):
            abs_path = self.__cache.get_item(full_name)
        else:
            abs_path = self.__trotter.download(full_name_object.name, full_name_object.version)
            self.__cache.put_item(abs_path, full_name)

        return abs_path

    def __install_uninstalled_dependencies(self, docker, path, base_path):
        packet = FilePacket(path)
        meta = packet.meta
        
        for dependency in meta.depends:
            packet_fullname = self.__select_dependent_packet_full_name(dependency, meta)
            self.__install(packet_fullname, docker, Status.INSTALLING_AS_DEPENDENCY, base_path)

    def __get_dependent_versions(self, meta, name, versions):
        return [FullName(name, version) for version in versions if meta.is_in_depends(name, version)]

class GafferFactory:
    class __impl:
        def __init__(self, reader):
            self.__reader = reader
            self.__location_table_win = {"all_users": "ALLUSERSPROFILE", "user": "USERPROFILE"}
            self.__location_table_nix = {"user": "HOME"}
            self.__logger = logging.getLogger("GafferFactory")
            
        def get_gaffer(self, config_path = None, fromlocal = None, use_virtual_docker = False):
            if not config_path:                                       
                config_path = os.path.join(sys.exec_prefix, "Scripts", "gaffer_config.yaml")
            if not os.path.exists(config_path):    
                message = "Not found file '%s' for docker configuration!" % config_path   
                __logerror__(self.__logger, message)         
                raise GafferFactoryError(message)
            
            self.__logger.info("Configuration file '%s' will be used for gaffer" % config_path)
            options = self.__reader.read(config_path)
            docker_config_uri = None
            
            if "cache_folder" in options.keys():
                cache_folder = options["cache_folder"]
                self.__logger.debug("cache folder: %s" % cache_folder)
            if "cache_location" in options.keys():
                cache_location = options["cache_location"]
                self.__logger.debug("cache location: %s" % cache_location)
            if "docker_config" in options.keys():                
                docker_config_uri = options["docker_config_uri"]
                self.__logger.debug("docker_config_uri: %s" % docker_config_uri)
         
            if docker_config_uri:
                docker = DockerFactory().get_docker(docker_config_uri)                
            else:
                docker = DockerFactory().get_docker()
            if use_virtual_docker:
                docker = DockerFactory().get_virtual_docker(docker)

                
            if sys.platform == "win32":
                cache_location_path = os.environ[self.__location_table_win[cache_location]]                
            else:
                if cache_location == "all_users":
                    message = "Configuration option: %s for platform: %s is not supported" % (cache_location, sys.platform)
                    __logerror__(self.__logger, message)         
                    raise GafferFactoryError (message)                
                cache_location_path = os.environ[self.__location_table_nix[cache_location]]                
             
            cache_path = os.path.join(cache_location_path, cache_folder)
            cache = Cache(cache_path)
            if fromlocal:   
                options["fromlocal"] = fromlocal
            trotter = TrotterFactory().get_trotter(options)
            catalogue = Catalogue(trotter)
            select_strategy = LatestStrategy()            

            return Gaffer(cache, docker, trotter, catalogue, select_strategy)

    __instance = None

    def __init__(self, reader = ConfigReader()):
        if GafferFactory.__instance is None:
            GafferFactory.__instance = GafferFactory.__impl(reader)

        self.__dict__['_GafferFactory__instance'] = GafferFactory.__instance

    def __getattr__(self, attr):
        return getattr(self.__instance, attr)

    def __setattr__(self, attr, value):
        return setattr(self.__instance, attr, value)
